﻿using FastMember;
using FullSearch.Common;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Facet;
using Lucene.Net.Facet.Taxonomy;
using Lucene.Net.Facet.Taxonomy.Directory;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using static Lucene.Net.Facet.Taxonomy.SearcherTaxonomyManager;

namespace FullSearch
{
    public class DocIndex
    {
        private readonly TrackingIndexWriter trackingIndexWriter;

        private readonly FSDirectory indexWriterDir;
        private readonly IndexWriter indexWriter;
        private readonly Task indexWriterFlushTask;
        private readonly Task indexWriterCommitTask;
        private readonly SearcherManager searcherManager;
        private readonly ControlledRealTimeReopenThread<IndexSearcher> realTimeThread;

        private readonly bool useFacet = false;
        private readonly FSDirectory taxonomyWriterDir;
        private readonly DirectoryTaxonomyWriter taxonomyWriter;
        private readonly Task taxonomyWriterCommitTask;
        private readonly SearcherTaxonomyManager searcherTaxonomyManager;
        private readonly ControlledRealTimeReopenThread<SearcherAndTaxonomy> realTimeTaxonomyThread;
        private readonly FacetsConfig facetsConfig;

        public DocIndex(string name, Analyzer analyzer = null, bool useFacet = false, string savePath = null)
        {
            this.useFacet = useFacet;
            var version = Lucene.Net.Util.LuceneVersion.LUCENE_48;
            if (analyzer == null)
            {
                analyzer = new StandardAnalyzer(version);
            }
            if (string.IsNullOrEmpty(savePath))
            {
                savePath = AppDomain.CurrentDomain.BaseDirectory + "index/";
            }
            string indexWriterDirName = savePath + name;
            indexWriterDir = FSDirectory.Open(indexWriterDirName);
            if (IndexWriter.IsLocked(indexWriterDir))
            {
                IndexWriter.Unlock(indexWriterDir);
            }
            var config = new IndexWriterConfig(version, analyzer)
            {
                OpenMode = OpenMode.CREATE_OR_APPEND
            };
            indexWriter = new IndexWriter(indexWriterDir, config);
            trackingIndexWriter = new TrackingIndexWriter(indexWriter);
            if (!useFacet)
            {
                searcherManager = new SearcherManager(indexWriter, true, new SearcherFactory());
                realTimeThread = new ControlledRealTimeReopenThread<IndexSearcher>(trackingIndexWriter, searcherManager, DocFactory.TargetMaxScaleSec, DocFactory.TargetMinStaleSec);
                realTimeThread.IsBackground = true;
                realTimeThread.Start();
            }
            else
            {
                facetsConfig = new FacetsConfig();
                string indexWriterTaxDirName = indexWriterDirName + "_taxonomy";
                taxonomyWriterDir = FSDirectory.Open(indexWriterTaxDirName);
                taxonomyWriter = new DirectoryTaxonomyWriter(taxonomyWriterDir, OpenMode.CREATE_OR_APPEND);
                DirectoryTaxonomyWriter.Unlock(taxonomyWriterDir);
                searcherTaxonomyManager = new SearcherTaxonomyManager(indexWriter, true, new SearcherFactory(), taxonomyWriter);
                realTimeTaxonomyThread = new ControlledRealTimeReopenThread<SearcherAndTaxonomy>(trackingIndexWriter, searcherTaxonomyManager, DocFactory.TargetMaxScaleSec, DocFactory.TargetMinStaleSec);
                realTimeTaxonomyThread.IsBackground = true;
                realTimeTaxonomyThread.Start();
            }

            #region Flush And Commot

            if (useFacet)
            {
                if (DocFactory.TaxonomyIndexWriterCommitSec != 0)
                {
                    var sec = DocFactory.TaxonomyIndexWriterCommitSec * 1000;
                    taxonomyWriterCommitTask = Task.Factory.StartNew(async () =>
                    {
                        while (true)
                        {
                            await Task.Delay(sec);
                            try
                            {
                                CommitTaxonomy();
                            }
                            catch { }
                        }

                    }, TaskCreationOptions.LongRunning);
                }
            }

            if (DocFactory.IndexWriterFlushSec != 0)
            {
                var sec = DocFactory.IndexWriterFlushSec * 1000;
                indexWriterFlushTask = Task.Factory.StartNew(async () =>
                {
                    while (true)
                    {
                        await Task.Delay(sec);
                        try
                        {
                            Flush(true, true);
                        }
                        catch { }
                    }

                }, TaskCreationOptions.LongRunning);
            }

            if (DocFactory.IndexWriterCommitSec != 0)
            {
                var sec = DocFactory.IndexWriterCommitSec * 1000;
                indexWriterCommitTask = Task.Factory.StartNew(async () =>
                {
                    while (true)
                    {
                        await Task.Delay(sec);
                        try
                        {
                            Commit();
                        }
                        catch { }
                    }

                }, TaskCreationOptions.LongRunning);
            }

            #endregion
        }

        #region Add

        private static Document CreateDocument<T>(T model, params DocFieldBoost[] boostList)
        {
            var doc = new Document();
            var wrapped = ObjectAccessor.Create(model);
            var t = typeof(T);
            bool stored = true;
            bool analyzer = false;
            bool docValues = true;
            bool facet = false;
            bool geo = false;
            foreach (var pro in t.GetProperties())
            {
                var ignoreAttr = pro.GetCustomAttribute<DocFieldIgnoreAttribute>();
                if (ignoreAttr != null)
                {
                    continue;
                }
                var fieldAttr = pro.GetCustomAttribute<DocFieldAttribute>();
                if (fieldAttr != null)
                {
                    stored = fieldAttr.Stored;
                    analyzer = fieldAttr.Analyzer;
                    docValues = fieldAttr.DocValues;
                    facet = fieldAttr.Facet;
                    geo = fieldAttr.Geo;
                }
                doc.Add(pro.Name, wrapped[pro.Name], pro.PropertyType, stored, analyzer, docValues, facet, geo, boostList);
            }
            return doc;
        }

        private static Document CreateDocument(IEnumerable<DocFieldEntity> entitys, params DocFieldBoost[] boostList)
        {
            var doc = new Document();
            foreach (var item in entitys)
            {
                doc.Add(item, boostList);
            }
            return doc;
        }

        public void InsertDocument(Document doc)
        {
            if (!useFacet)
            {
                indexWriter.AddDocument(doc);
            }
            else
            {
                var _doc = facetsConfig.Build(doc);
                indexWriter.AddDocument(_doc);
            }
        }

        public void InsertDocuments(List<Document> docList)
        {
            if (!useFacet)
            {
                indexWriter.AddDocuments(docList);
            }
            else
            {
                foreach (var doc in docList)
                {
                    var _doc = facetsConfig.Build(doc);
                    indexWriter.AddDocument(_doc);
                }
            }
        }

        public void Insert<T>(T model, params DocFieldBoost[] boostList)
        {
            InsertDocument(CreateDocument(model, boostList));
        }

        public void InsertMany<T>(IEnumerable<T> modelList, params DocFieldBoost[] boostList)
        {
            var docList = new List<Document>();
            foreach (var model in modelList)
            {
                docList.Add(CreateDocument(model, boostList));
            }
            InsertDocuments(docList);
        }

        public void Insert(IEnumerable<DocFieldEntity> entitys, params DocFieldBoost[] boostList)
        {
            InsertDocument(CreateDocument(entitys, boostList));
        }

        public void InsertMany(IEnumerable<IEnumerable<DocFieldEntity>> entitysList, params DocFieldBoost[] boostList)
        {
            var docList = new List<Document>();
            foreach (var model in entitysList)
            {
                docList.Add(CreateDocument(model, boostList));
            }
            InsertDocuments(docList);
        }

        #endregion

        #region Update

        public void UpdateDocument(Term term, Document doc)
        {
            indexWriter.UpdateDocument(term, doc);
        }

        public void Update<T>(T model, params DocFieldBoost[] boostList)
        {
            var key = DocCommmon.GetPrimaryKey<T>();
            var doc = CreateDocument(model, boostList);
            var wrapped = ObjectAccessor.Create(model);
            var id = wrapped[key];
            var t = id.GetType();
            if (t == typeof(string))
            {
                var term = new Term(key, (string)id);
                UpdateDocument(term, doc);
            }
            else
            {
                DeleteById<T>(id);
                InsertDocument(doc);
            }

        }

        public void UpdateMany<T>(IEnumerable<T> modelList, params DocFieldBoost[] boostList)
        {
            foreach (var item in modelList)
            {
                Update(item, boostList);
            }
        }

        public void Update(IEnumerable<DocFieldEntity> entitys, params DocFieldBoost[] boostList)
        {
            var doc = CreateDocument(entitys, boostList);
            var keyEntity = entitys.FirstOrDefault(f => f.IsKey == true);
            if (keyEntity.ValueType == "String")
            {
                var term = new Term(keyEntity.Name, (string)keyEntity.Value);
                UpdateDocument(term, doc);
            }
            else if (keyEntity.ValueType == "Int64")
            {
                var val = Convert.ToInt64(keyEntity.Value);
                var q = NumericRangeQuery.NewInt64Range(keyEntity.Name, val, val, true, true);
                DeleteDocuments(q);
                InsertDocument(doc);
            }
            else
            {
                var val = Convert.ToInt32(keyEntity.Value);
                var q = NumericRangeQuery.NewInt32Range(keyEntity.Name, val, val, true, true);
                DeleteDocuments(q);
                InsertDocument(doc);
            }

        }

        public void UpdateMany(IEnumerable<IEnumerable<DocFieldEntity>> entitysList, params DocFieldBoost[] boostList)
        {
            foreach (var item in entitysList)
            {
                Update(item, boostList);
            }
        }

        #endregion

        #region Delete

        public void DeleteDocuments(Term term)
        {
            indexWriter.DeleteDocuments(term);
        }

        public void DeleteDocuments(params Term[] terms)
        {
            indexWriter.DeleteDocuments(terms);
        }

        public void DeleteDocuments(Query query)
        {
            indexWriter.DeleteDocuments(query);
        }

        public void DeleteDocuments(params Query[] querys)
        {
            indexWriter.DeleteDocuments(querys);
        }

        public void DeleteById<T>(object id)
        {
            var key = DocCommmon.GetPrimaryKey<T>();
            var t = id.GetType();
            if (t == typeof(string))
            {
                Term term = new Term(key, (string)id);
                DeleteDocuments(term);
            }
            else if (t == typeof(int))
            {
                var val = (int)id;
                var q = NumericRangeQuery.NewInt32Range(key, val, val, true, true);
                DeleteDocuments(q);
            }
            else
            {
                var val = (long)id;
                var q = NumericRangeQuery.NewInt64Range(key, val, val, true, true);
                DeleteDocuments(q);
            }
        }

        public void DeleteAll()
        {
            indexWriter.DeleteAll();
        }

        #endregion

        #region Search

        public void SearchWrap(Action<IndexSearcher, DirectoryTaxonomyReader> action)
        {
            if (!useFacet)
            {
                var search = searcherManager.Acquire();
                try
                {
                    action(search, null);
                }
                catch
                {
                    throw;
                }
                finally
                {
                    searcherManager.Release(search);
                }
            }
            else
            {
                var sAndTax = searcherTaxonomyManager.Acquire();
                try
                {
                    action(sAndTax.Searcher, sAndTax.TaxonomyReader);
                }
                catch
                {
                    throw;
                }
                finally
                {
                    searcherTaxonomyManager.Release(sAndTax);
                }
            }
        }

        public TResult SearchWrap<TResult>(Func<IndexSearcher, DirectoryTaxonomyReader, TResult> func)
        {
            if (!useFacet)
            {
                var search = searcherManager.Acquire();
                try
                {
                    return func(search, null);
                }
                catch
                {
                    throw;
                }
                finally
                {
                    searcherManager.Release(search);
                }
            }
            else
            {
                var sAndTax = searcherTaxonomyManager.Acquire();
                try
                {
                    return func(sAndTax.Searcher, sAndTax.TaxonomyReader);
                }
                catch
                {
                    throw;
                }
                finally
                {
                    searcherTaxonomyManager.Release(sAndTax);
                }
            }
        }

        public TopDocs Search(Query query, int n)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.Search(query, n);
            });
        }

        public TopDocs Search(Query query, int n, Sort sort)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.Search(query, n, sort);
            });
        }

        public TopDocs Search(Query query, Filter filter, int n)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.Search(query, filter, n);
            });
        }

        public TopDocs Search(Query query, Filter filter, int n, Sort sort)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.Search(query, filter, n, sort);
            });
        }

        public TopDocs Search(Query query, Filter filter, int n, Sort sort, bool doDocScores, bool doMaxScore)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.Search(query, filter, n, sort, doDocScores, doMaxScore);
            });
        }

        public TopDocs SearchAfter(ScoreDoc after, Query query, int n)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.SearchAfter(after, query, n);
            });
        }

        public TopDocs SearchAfter(ScoreDoc after, Query query, int n, Sort sort)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.SearchAfter(after, query, n, sort);
            });
        }

        public TopDocs SearchAfter(ScoreDoc after, Query query, Filter filter, int n)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.SearchAfter(after, query, filter, n);
            });
        }

        public TopDocs SearchAfter(ScoreDoc after, Query query, Filter filter, int n, Sort sort)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.SearchAfter(after, query, filter, n, sort);
            });
        }

        public TopDocs SearchAfter(ScoreDoc after, Query query, Filter filter, int n, Sort sort, bool doDocScores, bool doMaxScore)
        {
            return SearchWrap((search, taxonomyReader) =>
            {
                return search.SearchAfter(after, query, filter, n, sort, doDocScores, doMaxScore);
            });
        }

        #endregion

        public void Commit()
        {
            indexWriter?.Commit();
        }

        public void CommitTaxonomy()
        {
            taxonomyWriter?.Commit();
        }

        public void Flush(bool triggerMerge, bool applyAllDeletes)
        {
            indexWriter?.Flush(triggerMerge, applyAllDeletes);
        }

        public void ForceMerge(int maxNumSegments)
        {
            indexWriter?.ForceMerge(maxNumSegments);
        }

        public long TotalDoc()
        {
            return indexWriter.NumDocs;
        }

        public void ShutDown()
        {
            Commit();
            indexWriterFlushTask?.Dispose();
            indexWriterCommitTask?.Dispose();
            if (!useFacet)
            {
                realTimeThread.Dispose();
                searcherManager.Dispose();
            }
            else
            {
                CommitTaxonomy();
                taxonomyWriterCommitTask?.Dispose();
                realTimeTaxonomyThread.Dispose();
                searcherTaxonomyManager.Dispose();
                taxonomyWriter.Dispose();
                taxonomyWriterDir.Dispose();
            }
            indexWriter.Dispose();
            indexWriterDir.Dispose();
        }
    }
}
